class Bilibili_PlayWithMe {

    SHA256 = new Hashes.SHA256;
    MD5 = new Hashes.MD5;

    static WS = null;
    static AppID = 0;
    static LiveRoomID = 0;
    static MID = 0;
    static Param_Timestamp = 0;
    static Param_Sign = 0;

    constructor() {
        let URLParam = window.location.search.substring(1).split('&');
        URLParam.forEach(str => {
            let KAndV = str.split('=');
            if (KAndV.length == 2) {
                switch (KAndV[0].toUpperCase) {
                    case "ACSKEY":
                        Bilibili_PlayWithMe.Authorizer.DeveloperAccessKey = KAndV[1];
                        break;
                    case "ACSECRET":
                        Bilibili_PlayWithMe.Authorizer.DeveloperAccessSecret = KAndV[1];
                        break;
                    case "ROOMID":
                        Bilibili_PlayWithMe.LiveRoomID = KAndV[1];
                        break;
                    case "MID":
                        Bilibili_PlayWithMe.MID = KAndV[1];
                        break;
                    case "TIMESTAMP":
                        Bilibili_PlayWithMe.Param_Timestamp = KAndV[1];
                        break;
                    case "SIGN":
                        Bilibili_PlayWithMe.Param_Sign = KAndV[1];
                        break;
                }
            }
        })

        if ((typeof Bilibili_PlayWithMe.Authorizer.DeveloperAccessKey == 'undefined' || typeof Bilibili_PlayWithMe.Authorizer.DeveloperAccessSecret == 'undefined' || !Bilibili_PlayWithMe.Authorizer.DeveloperAccessKey || !Bilibili_PlayWithMe.Authorizer.DeveloperAccessSecret) && (typeof BiliOpen_RemoteAuthorizerServer == 'undefined' || !BiliOpen_RemoteAuthorizerServer)) {
            document.body.innerHTML = "<div style='position: fixed;height: 100vh;width: 100vw;top: 0;left: 0;background: #FFF;color: #ff778f;display: flex;justify-content: center;align-items: center;'><div><h2>该社区项目可能需要您自己准备与提供开发者的AccessKey和AccessSecret</h2><p>如果您没有<a href='https://open-live.bilibili.com/'>哔哩哔哩直播开放平台</a>申请成为开发者</p><p> 本地网页请在HTML文件里的script块内配置，如：<br/><b>&lt;script&gt;<br/>Bilibili_PlayWithMe.Authorizer.DeveloperAccessKey = '[你的开发者AccessKey]';<br/>Bilibili_PlayWithMe.Authorizer.DeveloperAccessSecret = '[你的开发者AccessSecret]';<br/>&lt;/script&gt;</b></p><hr><p> 远程网页请在请求后追加参数，如：<br/>https://play-live.bilibili.com/plugins-full/1651198829890<b>?AcsKey=[你的开发者AccessKey]&AcsSecret=[你的开发者AccessSecret]</b></p><p>　<br/>　<br/>　<br/>　<br/>　<br/>　<br/>　<br/>　<br/>　<br/>　<br/>　<br/>　<br/>　<br/>　<br/></p></div></div>";
        }


    }



    /**
     * 探测当前插件运行环境，是否为直播姬
     * @returns Boolean值的判定结果
     */
    static is_LiveHime() { return window.location.href.includes("livehime_ts") };

    /**
     * 探测当前插件运行环境，是否为OBS
     * @returns Boolean值的判定结果
     */
    static is_OBS() { return navigator.userAgent.includes("OBS") };

    /**
     * 发出B站开放平台通用的插件激活事件请求
     * - 注意不调用可能会影响开放平台里项目的统计数据
     */
    async PluginStart() {

        // 请求的Body部分
        let RequestBody = {
            app_id: Bilibili_PlayWithMe.AppID,
            room_id: Bilibili_PlayWithMe.LiveRoomID
        };

        // 转为符合JSON标准的String字符串
        let RequestBodyStr = JSON.stringify(RequestBody);

        let Request = {
            method: "POST",
            body: RequestBodyStr
        }

        // 对应接口
        let Response = await fetch("https://live-open.biliapi.com/v1/app/pluginStart",
            // 请求签名
            this.Authorization(Request));
        switch (Response.status) {
            case 200:
                let ResponseJSON = await Response.json();
                console.log("PluginStart请求完毕");
                if (Bilibili_PlayWithMe.AuthorizationResponseCode(ResponseJSON, "PluginStart")) {
                    Bilibili_PlayWithMe.NewDanmaku({
                        "fans_medal_level": 21,
                        "fans_medal_name": "黑喵姐",
                        "fans_medal_wearing_status": false,
                        "guard_level": 0,
                        "msg": "连上了喵！",
                        "timestamp": 1650717881,
                        "uid": 3102384,
                        "uname": "猫裙少年泽远喵",
                        "uface": "http://i0.hdslb.com/bfs/face/7ced8612a3f3ef10e7238ee22b4c6948d3f53139.jpg",
                        "room_id": 4639581
                    });
                };
                break;
        }
    }

    /**
     * 
     */
    async PrepareWEBSocketConnection() {
        // 请求的Body部分
        let RequestBody = {
            app_id: Bilibili_PlayWithMe.AppID,
            room_id: Bilibili_PlayWithMe.LiveRoomID
        };

        // 转为符合JSON标准的String字符串
        let RequestBodyStr = JSON.stringify(RequestBody);

        let Request = {
            method: "POST",
            body: RequestBodyStr
        }

        // 对应接口
        let Response = await fetch("https://live-open.biliapi.com/v1/common/websocketInfo",
            // 请求签名
            this.Authorization(Request));
        switch (Response.status) {
            case 200:
                let ResponseJSON = await Response.json();
                console.log("WEBSocketInfo请求完毕");
                if (Bilibili_PlayWithMe.AuthorizationResponseCode(ResponseJSON, "WEBSocketInfo")) {
                    this.WS = new BiliBili_WEBSocket(ResponseJSON.data.auth_body);
                };
                break;
        }
    }

    /**
     * 
     * @param {RequestInit} Request 待签名的请求内容
     * - 
     * @returns 
     */
    Authorization(Request) {
        // 获取当前UNIXTimestamp秒级时间戳【注意不是毫秒】
        // https://c.runoob.com/front-end/852/
        let UNIXTs = Math.round(new Date() / 1000);
        // 生成一个随机数，B站系统靠这随机数判断是不是重复请求，所以相邻的请求里这个数必须不同
        // 位数没有强制要求，单纯觉得7位够用
        let RdmNum = parseInt(10000000 * Math.random());

        console.log("请求的Body部分\n" + Request.body);
        // 为请求的Body部分生成MD5
        let BodyMD5 = this.MD5.hex(Request.body);
        console.log("请求Body部分的MD5\n" + BodyMD5);


        // 用所有“x-bili”开头的专属请求头，按照字母顺序排列生成待签名字符串
        let BiliHeadStr = "x-bili-accesskeyid:" + Bilibili_PlayWithMe.Authorizer.DeveloperAccessKey +
            "\nx-bili-content-md5:" + BodyMD5 +
            "\nx-bili-signature-method:HMAC-SHA256" +
            "\nx-bili-signature-nonce:" + RdmNum +
            "\nx-bili-signature-version:1.0" +
            "\nx-bili-timestamp:" + UNIXTs;
        console.log("请求的待签名字符串\n" + BiliHeadStr);
        // 使用HMAC-SHA256算法以开发者AccessSecret生成签名指纹
        let BiliHeadStrHMAC = this.HMAC(BiliHeadStr);
        console.log("预计的签名哈希\n" + this.HMAC(BiliHeadStr));

        Request.headers = {
            // 设置请求头的固定部分，标定内容为JSON
            "Accept": "application/json",
            "Content-type": "application/json",
            // 放入B站专属请求头
            "x-bili-accesskeyid": Bilibili_PlayWithMe.Authorizer.DeveloperAccessKey,
            "x-bili-content-md5": BodyMD5,
            "x-bili-signature-method": "HMAC-SHA256",
            "x-bili-signature-nonce": RdmNum.toString(),
            "x-bili-signature-version": "1.0",
            "x-bili-timestamp": UNIXTs.toString(),
            // 放入HMAC-SHA256算法对上面B站专属请求头进行签名得到的64位十六进制小写值【256位共64个字】
            "Authorization": BiliHeadStrHMAC,
        };
        return Request;
    }

    HMAC(Str) {
        return this.SHA256.hex_hmac(Bilibili_PlayWithMe.Authorizer.DeveloperAccessSecret, Str);
    }

    /**
     * 解析返回的JSON，在Log里提供信息
     * @param {JSON} ResponseJSON 返回的JSON
     * @param {String} Name 请求的接口名，用于Log显示
     * @returns {Boolean} 是否成功
     */
    static AuthorizationResponseCode(ResponseJSON, Name) {
        console.log(ResponseJSON);
        switch (ResponseJSON.code) {
            case 0:
                console.log(Name + "请求成功");
                return true;
            case 4012:
                console.log(Name + "请求鉴权失败：错误的MD5，请检查x-bili-content-md5");
                return false;
            case 4002:
                console.log(Name + "请求鉴权失败：错误的MD5，请检查Authorization的签名值\n必要时可访问：https://www.bilibili.com/read/cv16173511 \n使用上面的样本测试运算结果");
                return false;
            case 4003:
                console.log(Name + "请求鉴权失败：使用了过于古老的时间戳，请检查x-bili-timestamp的时间戳，B站要求十分钟内有效\n必要时可访问：https://c.runoob.com/front-end/852/ \n使用上面的工具将其与现实中的日期时间互转");
                return false;
            case 4004:
                console.log(Name + "请求鉴权失败：使用了固定的随机数或重复内容，请检查x-bili-signature-nonce，B站要求不能连续两次使用同一个随机数");
                return false;
            default:
                return false;
        }
    }

    static NewDanmaku() {

    }

    static NewGifts() {

    }

    /**
     * 
     */
    static Authorizer = {
        /**
         * 签名器类型
         * - "Local"：从本地寻找开发者AccessKey和AccessSecret
         * - 可以写在这里，也可以自动从页面请求参数中获取
         * - "Remote"：将签名流程委托给在线服务器
         * - 代签服务器端参照RemoteAuthorizer
         */
        Type: "",
        /**
         * 开发者AccessKey，必填
         */
        DeveloperAccessKey: "",
        /**
         * 开发者AccessSecret，使用在线服务器代签不用填
         */
        DeveloperAccessSecret: "",
        /**
         * 代签服务器接口地址，使用本地自签不用填
         */
        RemoteAuthorizerServer: ""
    }

}

/**
 * 
 */
class BiliBili_WEBSocket extends WebSocket {

    Reader = null;

    constructor(LoginAuthStr) {
        super("ws://broadcastlv.chat.bilibili.com:2244/sub")
        this.onopen = evt => {
            this.Login(LoginAuthStr);
            // 握手完毕后，先丢一个心跳包过去
            // 【你懂什么，这也是握手的一部分.gif】
            this.Heartbeat();
            // 设置自动定时发送心跳包
            setInterval(() => {
                this.Heartbeat();
                // 定时发送循环延迟，单位毫秒【20000=20秒】
            }, 20000);
        }


        // 收到消息回调
        this.onmessage = (evt) => {
            // 通过FileReader读出数据
            this.Reader = new FileReader();
            // 预先设定好，当数据读出后，送往NewMessage方法进行处理
            this.Reader.onloadend = (evt) => this.NewMessage(evt.currentTarget.result);
            // 命令FileReader开始读出
            this.Reader.readAsArrayBuffer(evt.data);
        }
    }

    /**
     * 
     * @param {*} LoginAuthStr 
     */
    Login(LoginAuthStr) {
        let Body = UtilTools.Str2Uint8Array(LoginAuthStr)
        let PackageLength = Body.length + 16;
        console.log("预计的包长度：" + PackageLength);
        let Data = new Array(0);
        // 放入包头
        Data.push.apply(Data, [
            // 【Packet Length】包长字节数(Byte)【每隔字节8个位(Bit)，即数组里的一个UInt8】
            // 【注意是包头(Header)+包身(Body)的整个数据长度】
            0, 0, (PackageLength > 255 ? 1 : 0), (PackageLength % 255),
            // 【Header Length】数据包包头长度16字节(Byte)【16个8位(Bit)】
            0, 16,
            // 【Version】协议版本0，无加密
            0, 0,
            // 【Operation】操作类型，7为OP_AUTH，登录握手包
            0, 0, 0, 7,
            // 【Sequence ID】保留字段，很多人都写1，我就跟着瞎写了
            0, 0, 0, 0
        ]);
        // 放入包身
        Data.push.apply(Data, Body);
        // 统一转换为Uint8Array
        Data = new Uint8Array(Data);

        console.log("[开放平台长链接]：准备发送握手包，内容为\n" + UtilTools.Uint8Array2HexStr(Data));
        // 调用原WEBSocket的发送方法发送握手包
        super.send(Data);
    }


    /**
     * 发送一个心跳包
     * - 必须每隔30秒发送一个，不然会被服务器认为客户端已经故障或关闭，切断连接
     */
    Heartbeat() {
        // 固定写死的心跳包
        let HeartbeatData = new Uint8Array([
            // 【Packet Length】包长18字节(Byte)【18个8位(Bit)】
            0, 0, 0, 18,
            // 【Header Length】数据包包头长度16字节(Byte)【16个8位(Bit)】
            0, 16,
            // 【Version】协议版本1，无加密【虽然文档里要求写0，但是正常是写1】
            0, 1,
            // 【Operation】操作类型，2为OP_HEARTBEAT，客户端心跳，KeepAlive包
            0, 0, 0, 2,
            // 【Sequence ID】保留字段，很多人都写1，我就跟着瞎写了
            0, 0, 0, 1,
            // 【Body】实际内容，其实是“{}”，无意义
            0x7b, 0x7d
        ]);
        console.log("[开放平台长链接]发送心跳包喵【￣^￣】");
        // 调用原WEBSocket的发送方法发送心跳包
        super.send(HeartbeatData);
    }

    /**
     * 
     * @param {*} Data 
     */
    NewMessage(Data) {

        let Uint8Ary = new Uint8Array(Data);
        console.log("[开放平台长链接]收到服务器的消息，内容为\n" + UtilTools.Uint8Array2HexStr(Uint8Ary));
        if (Data[7] == 2) {
            // TODO 需要Zlib解压的大包，还没做
        } else switch (Uint8Ary[11]) {
            case 3:
                console.log("[开放平台长链接]解析到服务器回复的心跳包");
                break;
            case 5:
                // 删掉头部后UTF8解码二进制内容
                let JSONStr = decodeUtf8(Data.slice(16)).trim();
                console.log("[开放平台长链接]解析到服务器的新消息\n" + JSONStr);
                let JSONObj = JSON.parse(JSONStr);
                switch (JSONObj.cmd) {

                    case "LIVE_OPEN_PLATFORM_DM":
                        // 当标识为 LIVE_OPEN_PLATFORM_DM
                        // 将 JSON 里的 data 传给 Bilibili_PlayWithMe.NewDanmaku() 方法
                        Bilibili_PlayWithMe.NewDanmaku(JSONObj.data);
                        break;

                    case "LIVE_OPEN_PLATFORM_SEND_GIFT":
                        // 当标识为 LIVE_OPEN_PLATFORM_SEND_GIFT
                        // 将 JSON 里的 data 传给 Bilibili_PlayWithMe.NewGifts() 方法
                        Bilibili_PlayWithMe.NewGifts(JSONObj.data);
                        break;
                }
                break;
            case 8:
                console.log("[开放平台长链接]解析到服务器的登陆回复");
                break;
            default:

                break;
        }
    }
}

/**
 * 静态工具类
 */
class UtilTools {



    /**
     * 转换Uint8Array为HEX字符串
     * @param {Uint8Array} Uint8Array 二进制数组
     * @returns 十六进制小写字符串
     */
    static Uint8Array2HexStr(Uint8Array) {
        return Array.prototype.map.call(Uint8Array, (x) => ('00' + x.toString(16)).slice(-2)).join(' ');
    }

    /**
     * 
     * @param {String} Str 字符串
     * @returns UTF-8编码下的原始数据Uint8Array2Str
     */
    static Str2Uint8Array(Str) {
        var arr = [];
        for (var i = 0, j = Str.length; i < j; ++i)
            arr.push(Str.charCodeAt(i));
        return new Uint8Array(arr);
    }
}